<!DOCTYPE html>
<html lang="zh-Hans">
  <head hexo-theme='https://github.com/volantis-x/hexo-theme-volantis/tree/4.3.1'>
  <meta charset="utf-8">
  <!-- SEO相关 -->
  
    
  
  <!-- 渲染优化 -->
  <meta http-equiv='x-dns-prefetch-control' content='on' />
  <link rel='dns-prefetch' href='https://cdn.jsdelivr.net'>
  <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
  <meta name="renderer" content="webkit">
  <meta name="force-rendering" content="webkit">
  <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
  <meta name="HandheldFriendly" content="True" >
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  
  <link rel="preload" href="/android-note/css/first.css" as="style">
  

  <!-- 页面元数据 -->
  
  <title>Android 性能优化总结 - Xander&#39;s blog</title>
  
    <meta name="keywords" content="Android,Performance">
  

  

  <!-- feed -->
  

  <!-- import meta -->
  

  <!-- link -->
  

  <!-- import link -->
  
  
  <link rel="shortcut icon" href="/android-note/images/xander_blog.ico">
  

  
    
<link rel="stylesheet" href="/android-note/css/first.css">

  

  
  <link rel="stylesheet" href="/android-note/css/style.css" media="print" onload="this.media='all';this.onload=null">
  <noscript><link rel="stylesheet" href="/android-note/css/style.css"></noscript>
  

  <script id="loadcss"></script>

  
<script>
if (/*@cc_on!@*/false || (!!window.MSInputMethodContext && !!document.documentMode))
    document.write(
	'<style>'+
		'html{'+
			'overflow-x: hidden !important;'+
			'overflow-y: hidden !important;'+
		'}'+
		'.kill-ie{'+
			'text-align:center;'+
			'height: 100%;'+
			'margin-top: 15%;'+
			'margin-bottom: 5500%;'+
		'}'+
	'</style>'+
    '<div class="kill-ie">'+
        '<h1><b>抱歉，您的浏览器无法访问本站</b></h1>'+
        '<h3>微软已经于2016年终止了对 Internet Explorer (IE) 10 及更早版本的支持，<br/>'+
        '继续使用存在极大的安全隐患，请使用当代主流的浏览器进行访问。</h3><br/>'+
        '<a target="_blank" rel="noopener" href="https://www.microsoft.com/zh-cn/WindowsForBusiness/End-of-IE-support"><strong>了解详情 ></strong></a>'+
    '</div>');
</script>


<noscript>
	<style>
		html{
			overflow-x: hidden !important;
			overflow-y: hidden !important;
		}
		.kill-noscript{
			text-align:center;
			height: 100%;
			margin-top: 15%;
			margin-bottom: 5500%;
		}
	</style>
    <div class="kill-noscript">
        <h1><b>抱歉，您的浏览器无法访问本站</b></h1>
        <h3>本页面需要浏览器支持（启用）JavaScript</h3><br/>
        <a target="_blank" rel="noopener" href="https://www.baidu.com/s?wd=启用JavaScript"><strong>了解详情 ></strong></a>
    </div>
</noscript>

</head>

  <body>
    

<header id="l_header" class="l_header auto shadow blur show" style='opacity: 0' >
  <div class='container'>
  <div id='wrapper'>
    <div class='nav-sub'>
      <p class="title"></p>
      <ul class='switcher nav-list-h m-phone' id="pjax-header-nav-list">
        <li><a id="s-comment" class="fas fa-comments fa-fw" target="_self" href='javascript:void(0)'></a></li>
        
          <li><a id="s-toc" class="s-toc fas fa-list fa-fw" target="_self" href='javascript:void(0)'></a></li>
        
      </ul>
    </div>
		<div class="nav-main">
      
        
        <a class="title flat-box" target="_self" href='/android-note/'>
          
            <img no-lazy class='logo' src='https://i.loli.net/2020/05/18/WO1oJwFsRb4gV79.png'/>
          
          
          
        </a>
      

			<div class='menu navigation'>
				<ul class='nav-list-h m-pc'>
          
          
          
            
            
              <li>
                <a class="menuitem flat-box faa-parent animated-hover" href=/android-note/
                  
                  
                  
                    id="android-note"
                  >
                  <i class='fas fa-book fa-fw'></i>BLOG
                </a>
                
              </li>
            
          
          
            
            
              <li>
                <a class="menuitem flat-box faa-parent animated-hover" href=/android-note/categories/
                  
                  
                  
                    id="android-notecategories"
                  >
                  <i class='fas fa-folder-open fa-fw'></i>分类
                </a>
                
              </li>
            
          
          
            
            
              <li>
                <a class="menuitem flat-box faa-parent animated-hover" href=/android-note/tags/
                  
                  
                  
                    id="android-notetags"
                  >
                  <i class='fas fa-tags fa-fw'></i>标签
                </a>
                
              </li>
            
          
          
            
            
              <li>
                <a class="menuitem flat-box faa-parent animated-hover" href=/android-note/archives/
                  
                  
                  
                    id="android-notearchives"
                  >
                  <i class='fas fa-archive fa-fw'></i>归档
                </a>
                
              </li>
            
          
          
            
            
              <li>
                <a class="menuitem flat-box faa-parent animated-hover" href=/android-note/about/
                  
                  
                  
                    id="android-noteabout"
                  >
                  <i class='fas fa-info-circle fa-fw'></i>关于
                </a>
                
              </li>
            
          
          
            
            
              <li>
                <a class="menuitem flat-box header toggle-mode-btn">
                  <i class='fas fa-moon fa-fw'></i>暗黑模式
                </a>
              <li>
            
          
          
				</ul>
			</div>

      <div class="m_search">
        <form name="searchform" class="form u-search-form">
          <i class="icon fas fa-search fa-fw"></i>
          <input type="text" class="input u-search-input" placeholder="Search..." />
        </form>
      </div>

			<ul class='switcher nav-list-h m-phone'>
				
					<li><a class="s-search fas fa-search fa-fw" target="_self" href='javascript:void(0)'></a></li>
				
				<li>
          <a class="s-menu fas fa-bars fa-fw" target="_self" href='javascript:void(0)'></a>
          <ul class="menu-phone list-v navigation white-box">
            
              
            
              <li>
                <a class="menuitem flat-box faa-parent animated-hover" href=/android-note/
                  
                  
                  
                    id="android-note"
                  >
                  <i class='fas fa-book fa-fw'></i>BLOG
                </a>
                
              </li>
            
          
            
              
            
              <li>
                <a class="menuitem flat-box faa-parent animated-hover" href=/android-note/categories/
                  
                  
                  
                    id="android-notecategories"
                  >
                  <i class='fas fa-folder-open fa-fw'></i>分类
                </a>
                
              </li>
            
          
            
              
            
              <li>
                <a class="menuitem flat-box faa-parent animated-hover" href=/android-note/tags/
                  
                  
                  
                    id="android-notetags"
                  >
                  <i class='fas fa-tags fa-fw'></i>标签
                </a>
                
              </li>
            
          
            
              
            
              <li>
                <a class="menuitem flat-box faa-parent animated-hover" href=/android-note/archives/
                  
                  
                  
                    id="android-notearchives"
                  >
                  <i class='fas fa-archive fa-fw'></i>归档
                </a>
                
              </li>
            
          
            
              
            
              <li>
                <a class="menuitem flat-box faa-parent animated-hover" href=/android-note/about/
                  
                  
                  
                    id="android-noteabout"
                  >
                  <i class='fas fa-info-circle fa-fw'></i>关于
                </a>
                
              </li>
            
          
            
              
            
              <li>
                <a class="menuitem flat-box header toggle-mode-btn">
                  <i class='fas fa-moon fa-fw'></i>暗黑模式
                </a>
              <li>
            
          
            
          </ul>
        </li>
			</ul>
		</div>
	</div>
  </div>
</header>

    <div id="l_body">
      <div id="l_cover">
  
    
        <div id="full" class='cover-wrapper post dock' style="display: none;">
          
            <div id='cover-backstretch'></div>
          
          <div class='cover-body'>
  <div class='top'>
    
    
      <p class="title">Xander's Blog</p>
    
    
      <p class="subtitle">A blog for Android</p>
    
  </div>
  <div class='bottom'>
    <div class='menu navigation'>
      <div class='list-h'>
        
          
            <a href="/android-note/"
              
              
              id="android-note">
              <i class='fas fa-book fa-fw'></i><p>BLOG</p>
            </a>
          
            <a href="/android-note/categories/"
              
              
              id="android-notecategories">
              <i class='fas fa-folder-open fa-fw'></i><p>分类</p>
            </a>
          
            <a href="/android-note/tags/"
              
              
              id="android-notetags">
              <i class='fas fa-tags fa-fw'></i><p>标签</p>
            </a>
          
            <a href="/android-note/archives/"
              
              
              id="android-notearchives">
              <i class='fas fa-archive fa-fw'></i><p>归档</p>
            </a>
          
            <a href="/android-note/about/"
              
              
              id="android-noteabout">
              <i class='fas fa-info-circle fa-fw'></i><p>关于</p>
            </a>
          
        
      </div>
    </div>
  </div>
</div>

          <div id="scroll-down" style="display: none;"><i class="fa fa-chevron-down scroll-down-effects"></i></div>
        </div>
    
  
  </div>

      <div id="safearea">
        <div class="body-wrapper" id="pjax-container">
          

<div class='l_main'>
  <article class="article post white-box reveal md shadow article-type-post" id="post" itemscope itemprop="blogPost">
  


  
  <div class="article-meta" id="top">
    
    
    
      <h1 class="title">
        Android 性能优化总结
      </h1>
      <div class='new-meta-box'>
        
          
            
<div class='new-meta-item author'>
  <a class='author' target="_blank" href="https://github.com/XanderWang/" rel="nofollow noopener">
    <img no-lazy src="https://i.loli.net/2020/05/18/WO1oJwFsRb4gV79.png">
    <p>XanderWang</p>
  </a>
</div>

          
        
          
            
  <div class='new-meta-item category'>
    <a class='notlink'>
      <i class="fas fa-folder-open fa-fw" aria-hidden="true"></i>
      <a class="category-link" href="/android-note/categories/Android-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/">Android 性能优化</a>
    </a>
  </div>


          
        
          
            <div class="new-meta-item date">
  <a class='notlink'>
    <i class="fas fa-calendar-alt fa-fw" aria-hidden="true"></i>
    <p>发布于：Apr 13, 2021</p>
  </a>
</div>

          
        
          
            
  <div class="new-meta-item browse leancloud">
    <a class='notlink'>
      
      <div id="lc-pv" data-title="Android 性能优化总结" data-path="/android-note/2021/04/13/android_performance/android_performance_all_in_one/">
        <i class="fas fa-eye fa-fw" aria-hidden="true"></i>
        <span id='number'><i class="fas fa-circle-notch fa-spin fa-fw" aria-hidden="true"></i></span>
        次浏览
      </div>
    </a>
  </div>


          
        
      </div>
    
  </div>


  
  
  <p>性能的优化是一个老生常谈的点，也是一个比较重要的点。做过一点性能优化的工作，现在对工作中的优化点做一个总结。如有错误，还请指正。</p>
<h1 id="有哪些方面需要优化"><a href="#有哪些方面需要优化" class="headerlink" title="有哪些方面需要优化"></a>有哪些方面需要优化</h1><p>在平时的优化过程中我们需要从哪几个点来优化呢？其实我们平时自己一定也用过软件，在使用软件的过程中有没有什么想吐槽的呢？</p>
<p>“这个 app 怎么还没下载完！”、“太卡了吧！”、”图片怎么还没加载出来！”、”怎么刚进去就卡了！”、“这么点了一下就退出了！”等等，是不是有这样的想法？这些想法其实包含了我们今天要说的内容，就是从哪些方面来优化我们的 APP ，我总结了以下几点。</p>
<ul>
<li>APK 瘦身优化</li>
<li>启动速度优化</li>
<li>稳定性优化 </li>
<li>内存的优化 </li>
<li>操作流畅度优化 </li>
</ul>
<p>当然，需要优化的不仅仅是这几个方面，我做的比较多的优化是这几个方面，暂时就这几个方面来谈谈优化吧。</p>
<h1 id="APK-瘦身"><a href="#APK-瘦身" class="headerlink" title="APK 瘦身"></a>APK 瘦身</h1><h2 id="如何查看-APK-的组成"><a href="#如何查看-APK-的组成" class="headerlink" title="如何查看 APK 的组成"></a>如何查看 APK 的组成</h2><p>如果要优化 APK 的大小，我们首先需要知道我们编译出来的 APK 都包含哪些东西，然后针对占用大的做裁剪，或者删除不需要的东西，从而达到瘦身的目的。</p>
<p>查看 APK 的内容占用情况很简单，打开 AS ，把 APK 拖到 AS 里面就可以查看 APK 包含的内容了。</p>
<p><img src="https://img.imgdb.cn/item/607455ab8322e6675c0bab2c.jpg" class="lazyload placeholder" data-srcset="https://img.imgdb.cn/item/607455ab8322e6675c0bab2c.jpg" srcset="https://cdn.jsdelivr.net/gh/volantis-x/cdn-volantis@3/img/placeholder/c617bfd2497fcea598e621413e315c368f8d8e.svg" alt="资源预览"></p>
<p>可以看到占大头的是 res 代码等，所以瘦身可以从这几个方面来考虑。</p>
<h2 id="如何减少-res-资源大小"><a href="#如何减少-res-资源大小" class="headerlink" title="如何减少 res 资源大小"></a>如何减少 res 资源大小</h2><ol>
<li>删除冗余的资源</li>
</ol>
<p>一般随着项目的迭代，部分图片等资源不再使用了，但是可能仍然被编译到了 apk 里面，所以可以删除这部分不再使用的资源，可以使用 lint 工具来搜索项目中不再使用的图片等资源。</p>
<ol start="2">
<li>重复资源的优化</li>
</ol>
<p>除了有冗余资源，还有些是文件名不一样，但是内容一样的图片，可以通过比较 md5 值来判断是不是一样的资源，然后编辑 resources.arsc 来重定向。</p>
<ol start="3">
<li>图片压缩</li>
</ol>
<p>未压缩的图片文件占用空间较大，可以考虑压缩未压缩过的图片来瘦身。常用的工具是  <a target="_blank" rel="noopener" href="https://tinypng.com/">tinypng</a> 网站。</p>
<p>同时也可以借助 <a target="_blank" rel="noopener" href="https://github.com/Deemonser/TinyPngPlugin">TinyPngPlugin</a> 等插件或者其他开源工具来帮助压缩图片。</p>
<ol start="4">
<li>资源混淆</li>
</ol>
<p>通过将资源路径 <code>res/drawable/wechat</code> 变为 <code>r/d/a</code> 的方式来减少 apk 的大小，当 apk 有较多资源项的时候，效果比较明显，这是一款微信开源的工具，详细地址是：<a target="_blank" rel="noopener" href="https://github.com/shwenzhang/AndResGuard">AndResGuard</a></p>
<ol start="5">
<li>指定语言</li>
</ol>
<p>如果没有特殊的需求的话，可以只编译中文，因为其他的语言用不上，如果用不上的语言编译了，会在 resource 的表里面占用大量的空间，故</p>
<pre><code>android &#123;
    defaultConfig &#123;
        ...
        // 仅支持 中文
        resConfigs &quot;zh&quot; 
    &#125;
&#125;
</code></pre>
<h2 id="如何减少-so-库资源大小"><a href="#如何减少-so-库资源大小" class="headerlink" title="如何减少 so 库资源大小"></a>如何减少 so 库资源大小</h2><ol>
<li>自己编译的 so </li>
</ol>
<p>release 包的  so 中移除调试符号。可以使用 Android NDK 中提供的 <code>arm-eabi-strip</code> 工具从原生库中移除不必要的调试符号。</p>
<p>如果是 cmake 来编译的话，可以再编辑脚本添加如下代码</p>
<pre><code>set(CMAKE_C_FLAGS_RELEASE &quot;$&#123;CMAKE_C_FLAGS_RELEASE&#125; -s&quot;)
set(CMAKE_CXX_FLAGS_RELEASE &quot;$&#123;CMAKE_CXX_FLAGS_RELEASE&#125; -s&quot;)
</code></pre>
<ol start="2">
<li>别人编译的 so</li>
</ol>
<p>联系作者修改，一般很难联系到。</p>
<ol start="3">
<li>动态下发 so</li>
</ol>
<p>可以通过服务器下发 so , 下载完后再进入应用，但是体验不好，但是是一个思路。</p>
<ol start="4">
<li>只编译指定平台的 so </li>
</ol>
<p>一般我们都是给 arm 平台的机器开发，如果没有特殊情况，我们一般只需要考虑 arm 平台的。具体的方法是 app 下的 build.gradle 添加如下代码</p>
<pre><code>android &#123;
    defaultConfig &#123;
        ndk &#123;
            abiFilter &quot;armeabi&quot;
        &#125;
    &#125;
&#125;
</code></pre>
<p>各个平台的差别如下：</p>
<table>
<thead>
<tr>
<th>平台</th>
<th>说明</th>
</tr>
</thead>
<tbody><tr>
<td>armeabi-v7a</td>
<td>arm 第 7 代及以上的处理器，2011 年后的设备基本都是</td>
</tr>
<tr>
<td>arm64-v8a</td>
<td>arm 第 8 代 64 位处理器设备</td>
</tr>
<tr>
<td>armeabi</td>
<td>arm 第 5、6 代处理器，早期的机器都是这个平台</td>
</tr>
<tr>
<td>x86</td>
<td>x86 32 位平台，平板和模拟器用的多</td>
</tr>
<tr>
<td>x86_64</td>
<td>x86 64 位平台</td>
</tr>
</tbody></table>
<h2 id="如何减少代码资源大小"><a href="#如何减少代码资源大小" class="headerlink" title="如何减少代码资源大小"></a>如何减少代码资源大小</h2><ol>
<li>一个功能尽量用一个库</li>
</ol>
<p>比如加载图片库，不要 glide 和 fresco 混用，因为功能是类似的，只是使用的方法不一样，用了多个库来做类似的事情，代码肯定就变多了。</p>
<ol start="2">
<li>混淆</li>
</ol>
<p>混淆的话，减少了生成的 class 大小，这样积少成多，也可以从一定层度减少 apk 的大小。</p>
<ol start="3">
<li>R 文件内联</li>
</ol>
<p>通过把 R 文件里面的资源内联到代码中，从而减少 R 文件的大小。</p>
<p>可以使用 <a target="_blank" rel="noopener" href="https://github.com/bytedance/ByteX/blob/master/shrink-r-plugin/README-zh.md">shrink-r-plugin</a> 工具来做 R 文件的内联</p>
<h2 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h2><p><a target="_blank" rel="noopener" href="https://tech.meituan.com/2017/04/07/android-shrink-overall-solution.html">Android App包瘦身优化实践</a></p>
<h1 id="启动速度"><a href="#启动速度" class="headerlink" title="启动速度"></a>启动速度</h1><h2 id="启动的类型"><a href="#启动的类型" class="headerlink" title="启动的类型"></a>启动的类型</h2><p>一般分为，冷启动和热启动</p>
<blockquote>
<p>冷启动：启动时，后台没有任何该应用的进程，系统需要重新创建一个进程，并结合启动参数启动该应用。</p>
</blockquote>
<blockquote>
<p>热启动：启动时，系统已经有该应用的进程(比如按 home 键临时退出该应用)下启动该应用。</p>
</blockquote>
<h2 id="如何获取启动时间"><a href="#如何获取启动时间" class="headerlink" title="如何获取启动时间"></a>如何获取启动时间</h2><ol>
<li>adb 命令</li>
</ol>
<p>adb shell am start -S -W 包名/启动类的全名</p>
<pre><code>adb shell am start -S -W xxx/xxxActivity
Stopping: xxx
Starting: Intent &#123; act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=xxx/xxxActivity &#125;
Status: ok
Activity: xxx/xxxActivity
ThisTime: 770
TotalTime: 770
WaitTime: 848
Complete
</code></pre>
<p><strong>ThisTime</strong>: 表示最后一个 Activity 启动时间</p>
<p><strong>TotalTime</strong>: 表示启动过程中，所有的 Activity 的启动时间</p>
<p><strong>WaitTime</strong>: 表示应用进程的创建时间 + TotalTime</p>
<p>一般我们关注 <code>TotalTime</code> 就好了。</p>
<p>另外，谷歌在 Android4.4（API 19）上也提供了测量方法，在 logcat 中过滤 Displayed 字段也可以看到启动时间</p>
<blockquote>
<p>2021-04-06 19:25:52.803 2210-2245 I/ActivityManager: Displayed xxx/xxxActivity: +623ms</p>
</blockquote>
<p><code>+623ms</code> 就是Activity 的启动时间。</p>
<ol start="2">
<li>时间戳</li>
</ol>
<p>时间戳的方法基于以下的 2 个知识点。</p>
<ul>
<li>应用进程刚创建，会调用 Application 的 onCreate 方法。</li>
<li>首次进入一个 Activity 后会在 onResume() 方法后面调用 onWindowsFocusChange 方法。</li>
</ul>
<p>结合这 2 个特性，我们可以在<br>A<br>Application 的 onCreate() 方法和 Activity 的 onWindowsFocusChange 方法里面，通过时间戳来获取应用的冷启动时间。</p>
<h2 id="如何监控启动过程"><a href="#如何监控启动过程" class="headerlink" title="如何监控启动过程"></a>如何监控启动过程</h2><ol>
<li>systrace</li>
</ol>
<p>systrace 是一个功能很强大的工具，除了可以查看卡顿问题，也可以用来查看应用的启动问题。使用示例如下：</p>
<blockquote>
<p>python $ANDROID_HOME/platform-tools/systrace/systrace.py gfx view wm am pm ss dalvik app sched -b 90960 -a 你的包名 -o test.log.html</p>
</blockquote>
<p>用 Google  浏览器打开 <code>test.log.html</code> 就可以看到详细的启动信息。</p>
<ol start="2">
<li>Debug 接口</li>
</ol>
<pre><code>package android.os;
...
class Debug &#123;
    ...
    public static void startMethodTracingSampling(String tracePath, int bufferSize, int intervalUs) &#123;

    &#125;
    public static void startMethodTracing(String tracePath, int bufferSize) &#123;

    &#125;
&#125;
</code></pre>
<p>利用 Debug 类的这两个方法，可以生成一个 <code>trace</code> 文件，这个 <code>trace</code> 文件，可以直接在 <code>AS</code> 里面打开，可以看到从 <code>startMethodTracingSampling</code> 到 <code>startMethodTracing</code> 过程中的方法调用等信息，也可以较好的分析启动问题。</p>
<h2 id="一般有那些优化方法"><a href="#一般有那些优化方法" class="headerlink" title="一般有那些优化方法"></a>一般有那些优化方法</h2><ol>
<li>耗时操作放到异步进程</li>
</ol>
<p>比如文件解压、读写等耗时 IO 操作可以新开一个线程来执行。</p>
<ol start="2">
<li>延时初始化</li>
</ol>
<p>即暂时不适用的工具类等延后到使用的时候再去初始化。比如从 xml 里面读取颜色，可以考虑在使用的时候再去读取和解析。</p>
<ol start="3">
<li>线程优化</li>
</ol>
<p>线程的创建需要消耗较多的系统系统资源，减少线程的创建。可以考虑共用一个线程池。</p>
<p>如何检测线程的创建，可以参考我个开源库 <a target="_blank" rel="noopener" href="https://github.com/XanderWang/performance">performance</a></p>
<h1 id="稳定性优化"><a href="#稳定性优化" class="headerlink" title="稳定性优化"></a>稳定性优化</h1><h2 id="APP-稳定性的维度"><a href="#APP-稳定性的维度" class="headerlink" title="APP 稳定性的维度"></a>APP 稳定性的维度</h2><p>app 稳定一般指的是 app 能正常运行， app 不能正常运行的情况分为两大类，分别是 <code>Crash</code> 和 <code>ANR</code></p>
<blockquote>
<p>Crash：运行过程中发生的错误，是无法避免的。</p>
</blockquote>
<blockquote>
<p>ANR：应用再运行时，由于无法再规定的时间段内响应完，系统做出的一个操作。</p>
</blockquote>
<h2 id="如何治理-Crash"><a href="#如何治理-Crash" class="headerlink" title="如何治理 Crash"></a>如何治理 <code>Crash</code></h2><p>应用发生 Crash 是由于应用在运行时，应用产生了一个未处理的异常(就是没有被 try catch 捕获的异常)。这会导致 app 无法正常运行。</p>
<p>如果需要解决的话，就需要知道这个未处理的异常是在哪里产生的，一般是通过分析未处理的异常的方法调用堆栈来解决问题。</p>
<p>Android APP 可以分为 2 层，Java 层和 Native 层。所以如何捕获需要分开说。</p>
<h3 id="Java-层获取未处理的异常的调用堆栈"><a href="#Java-层获取未处理的异常的调用堆栈" class="headerlink" title="Java 层获取未处理的异常的调用堆栈"></a>Java 层获取未处理的异常的调用堆栈</h3><p>这个需要了解 <code>Java</code> 虚拟机是如何把一个未捕获的异常报上来的。</p>
<p>未捕获的异常，会沿着方法的调用链依次上抛，直到 <code>ThreadGroup</code> 的 <code>uncaughtException</code> 方法</p>
<pre><code class="java">    public void uncaughtException(Thread t, Throwable e) &#123;
        if (parent != null) &#123;
            // 递归调用，可以忽略
            parent.uncaughtException(t, e); 
        &#125; else &#123;
            // 交给了 Thread.getDefaultUncaughtExceptionHandler() 来处理未捕获的异常
            Thread.UncaughtExceptionHandler ueh =
                Thread.getDefaultUncaughtExceptionHandler();
            if (ueh != null) &#123;
                ueh.uncaughtException(t, e);
            &#125; else if (!(e instanceof ThreadDeath)) &#123;
                System.err.print(&quot;Exception in thread \&quot;&quot;
                                 + t.getName() + &quot;\&quot; &quot;);
                e.printStackTrace(System.err);
            &#125;
        &#125;
    &#125;
</code></pre>
<p>查阅代码发现，发现 <code>ThreadGroup</code> 最终会给 <code>Thread</code> 的 <code>defaultUncaughtExceptionHandler</code> 处理。</p>
<pre><code class="java">private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;
</code></pre>
<p> 上面的代码显示：<code>Thread</code> 的 <code>defaultUncaughtExceptionHandler</code> 是 <code>Thread</code> 类的一个静态变量。</p>
<p>看到这里，如何捕获 <code>Java</code> 层未处理的异常就很清晰了，给 <code>Thread</code> 设置一个<code>新的 defaultUncaughtExceptionHandler</code>，在这个新的<code> defaultUncaughtExceptionHandler</code> 里面收集需要的信息就可以了。</p>
<p>需要注意的一点是 <code>旧的 defaultUncaughtExceptionHandler</code> 需要保存下来，然后<code>新的 defaultUncaughtExceptionHandler</code> 收集信息后，需要再转给<code>旧的 defaultUncaughtExceptionHandler</code> 继续处理。</p>
<h3 id="Native-层获取未处理的异常的相关信息"><a href="#Native-层获取未处理的异常的相关信息" class="headerlink" title="Native 层获取未处理的异常的相关信息"></a>Native 层获取未处理的异常的相关信息</h3><p>Java 层如何收集未处理的异常的信息说过了，我们来看看 Native 层发生未处理的异常的话，是如何处理的。 Native 层的处理，需要掌握 linux 的一些知识，由于本人不是特别了解 linux ，这里就直接参考别人的文章了。如果有错误，还请指正。</p>
<p>本人通过查阅资料发现，Native 层如果发生未处理的异常(注:如果 Native 层捕获了异常，是可以通过 JNI 抛到 Java 层去处理的) ，系统会发出信号给 Native 层，在 Native 层如果要收集未处理的异常信息，就需要注册对应信号的处理函数。当发生异常的时候，Native 层会收到信息，然后通过处理器来收集信息。</p>
<p>注册信号处理函数如下：</p>
<pre><code class="c">#include &lt;signal.h&gt; 
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));
</code></pre>
<ul>
<li>signum：代表信号编码，可以是除SIGKILL及SIGSTOP外的任何一个特定有效的信号，如果为这两个信号定义自己的处理函数，将导致信号安装错误。</li>
<li>act：指向结构体sigaction的一个实例的指针，该实例指定了对特定信号的处理，如果设置为空，进程会执行默认处理。</li>
<li>oldact：和参数act类似，只不过保存的是原来对相应信号的处理，也可设置为NULL。</li>
</ul>
<p>有了信号处理函数，后面还要做的事情就是收集信息了，由于本人不是很熟悉 Native 的开发，这里就不展开说了了，大家可以参考 <a target="_blank" rel="noopener" href="https://www.cnblogs.com/mingfeng002/p/9118253.html">Android 平台 Native 代码的崩溃捕获机制及实现</a>。</p>
<h2 id="如何治理-ANR"><a href="#如何治理-ANR" class="headerlink" title="如何治理 ANR"></a>如何治理 <code>ANR</code></h2><p><code>ANR</code> 是 <code>Applicatipon No Response</code> 的简称。如果应用卡死或者响应过慢，系统就会杀死应用。为什么要杀死应用？其实也很好理解，如果不杀死应用，大家会以为系统坏了。</p>
<p>那我们如何监控 <code>ANR</code> 呢？以及我们如何分析 ANR 的问题呢？常见的导致 ANR 的原因有哪些呢？</p>
<p>首先，<code>ANR</code> 的原理是 <code>AMS</code> 在 <code>UI 操作</code>开始的时候，会根据 <code>UI 操作</code>的类型开启一个延时任务，如果这个任务被触发了，就表示应用卡死或者响应过慢。这个任务会在 <code>UI 操作</code>结束的时候被移除。</p>
<p>然后，如何分析 <code>ANR</code> 问题呢？</p>
<p>一般 <code>ANR</code> 发生的时候， <code>logcat</code> 里面会打印 <code>ANR</code> 相关的信息，过滤关键字 <code>ANR</code> 就可以看到，这里不做详细分析，可以参考后面的文章。</p>
<p>然后一般会在 <code>/data/anr</code> 目录下面生成 <code>traces.txt</code> 文件，里面一般包含了 <code>ANR</code> 发生的时候，系统和所有应用的线程等信息(需要注意的是，不同的 rom 可能都不一样)，通过 <code>logcat</code> 打印的信息和 <code>traces.txt</code> 里面的信息，大部分的 <code>ANR</code> 可以分析出原因，但是呢，也有相当一部分的 ANR 问题无法分析，因为 <code>logcat</code> 和 <code>traces.txt</code> 提供的信息有限，有时候甚至没有特别有用的信息，特别是 <code>Android</code> 的权限收紧， <code>traces.txt</code> 文件在<code>高 Android 版本</code>无法读取，给 <code>ANR</code> 问题的分析增加了不少的困难。不过好在最近发现头条给 <code>ANR</code> 写了一个系列的文章，里面对 ANR 问题的治理方法，个人觉得很好，这里引用一下。</p>
<ul>
<li><a target="_blank" rel="noopener" href="https://mp.weixin.qq.com/s/ApNSEWxQdM19QoCNijagtg">今日头条 ANR 优化实践系列 - 设计原理及影响因素</a></li>
<li><a target="_blank" rel="noopener" href="https://mp.weixin.qq.com/s/_Z6GdGRVWq-_JXf5Fs6fsw">今日头条 ANR 优化实践系列 - 监控工具与分析思路</a></li>
<li><a target="_blank" rel="noopener" href="https://mp.weixin.qq.com/s/4-_SnG4dfjMnkrb3rhgUag">今日头条 ANR 优化实践系列分享 - 实例剖析集锦</a></li>
<li><a target="_blank" rel="noopener" href="https://mp.weixin.qq.com/s/OBYWrUBkWwV8o6ChSVaCvw">今日头条 ANR 优化实践系列 - Barrier 导致主线程假死</a></li>
</ul>
<p>本人之前写过一个小的<a target="_blank" rel="noopener" href="https://github.com/XanderWang/performance">性能监测的工具</a>，其中有监控 <code>UI</code> 线程 <code>Block</code> 的功能，考虑后续加入头条的 <code>ANR</code> 监测机制，等后续完成了，在做一个详细的总结吧。这次的总结就写到这里。</p>
<h1 id="内存的优化"><a href="#内存的优化" class="headerlink" title="内存的优化"></a>内存的优化</h1><p>硬件的内存总是有限的，所有每个应用分到的内存也是有限的，所有内存的优化很有必要，否则应用就没有足够的内存使用了，这个时候就会 Crash 。</p>
<h2 id="内存都消耗在哪里了"><a href="#内存都消耗在哪里了" class="headerlink" title="内存都消耗在哪里了"></a>内存都消耗在哪里了</h2><p>优化内存的话，需要了解内存在哪里消耗了了，针对内存消耗大的场景做优化，对症下药，才可以有一个好的优化效果。</p>
<p><code>Android Studio</code> 里面的 <code>Profiler</code> 工具是一个很好用的工具，通过里面的 <code>memory</code> 工具可以<code>实时监控</code> APP 运行过程中的内存分配。</p>
<p><img src="https://img.imgdb.cn/item/60742b578322e6675cac02b9.jpg" class="lazyload placeholder" data-srcset="https://img.imgdb.cn/item/60742b578322e6675cac02b9.jpg" srcset="https://cdn.jsdelivr.net/gh/volantis-x/cdn-volantis@3/img/placeholder/c617bfd2497fcea598e621413e315c368f8d8e.svg" alt="内存性能分析器"></p>
<p>dump APP 内存堆栈后，还可以看到各个类占用的内存情况。</p>
<p><img src="https://img.imgdb.cn/item/60742b928322e6675cac8841.jpg" class="lazyload placeholder" data-srcset="https://img.imgdb.cn/item/60742b928322e6675cac8841.jpg" srcset="https://cdn.jsdelivr.net/gh/volantis-x/cdn-volantis@3/img/placeholder/c617bfd2497fcea598e621413e315c368f8d8e.svg" alt="有关每个已分配对象的详细信息显示在右侧的 Instance View 中"></p>
<p>可以查看每个对象的详细信息。</p>
<p><code>Android Studio</code> 里面的 <code>Profiler</code> 工具的具体使用教程请参考<a target="_blank" rel="noopener" href="https://developer.android.google.cn/studio/profile/memory-profiler?hl=zh-cn">官方教程</a>，这里就不做详细介绍了。</p>
<h2 id="如何合理使用内存"><a href="#如何合理使用内存" class="headerlink" title="如何合理使用内存"></a>如何合理使用内存</h2><p>利用上面的方法，找到内存消耗大的场景，就需要做优化了，主要做法就是想办法减少特定场景下的内存的使用。个人总结了一下平时可能会做的优化。</p>
<ul>
<li>图片相关的优化</li>
</ul>
<p>图片是我目前做的应用里面占用内存比较大的一块了，也碰到了一些问题，我主要是通过以下的方法来做优化。</p>
<ol>
<li>暂时用不上的图片不加载，比如说，有个网络加载异常的图，不要一开始就初始化，等到真的有异常了需要展示的时候再初始化</li>
<li>加载图片的时候，尽量加载指定大小的图片，因为有时候会碰到控件的大小小于实际图片尺寸的情况，这个时候，会浪费一些内存。有需要的话，可以让后台返回不同尺寸的图片。</li>
<li>根据不同的图片格式</li>
<li>不显示的图片，可以考虑先释放。</li>
</ol>
<ul>
<li>尽可能少地创建对象</li>
</ul>
<p>毫无疑问，如果对象少，内存肯定也消耗的少，那平时需要注意哪些呢？</p>
<ol>
<li>自定义 view 的时候，不要在 onDraw 方法里面频繁创建对象。因为 onDraw 方法可能会频繁调用，这个时候就会创建大量的对象。从而造成浪费，同时也会导致 gc 触发的频率升高，造成卡顿。</li>
<li>尽量少创建线程，创建线程其实是比较消耗资源的，创建一个空的线程，大概会占用 1-2 M 内存。同时一般异步任务很快就会执行完，如果频繁创建线程来做异步任务，除了内存使用的多，还可能 GC 造成卡顿。执行异步任务的话，一般建议用线程池来执行，但是需要注意线程池的使用。</li>
<li>尽量用 StringBuilder 或者 StringBuffer 来拼接字符串。平时发现的问题主要是在打印 logcat 的时候和拼接后台返回的数据的时候会创建大量的 String，所以如果有类似的情况也可以考虑做一些优化。</li>
</ol>
<h2 id="内存泄漏是什么"><a href="#内存泄漏是什么" class="headerlink" title="内存泄漏是什么"></a>内存泄漏是什么</h2><p>内存泄漏指的是本应该释放的内存，由于一些原因，被 GC ROOT 对象持有，从而而无法在 GC 的时候释放，这样可能会导致的一个问题就是，重复操作以后，APP 没有足有的内存使用了，这个时候系统会杀死 APP 。所以内存泄漏是需要排查的。</p>
<h2 id="如何监控和分析内存泄漏问题"><a href="#如何监控和分析内存泄漏问题" class="headerlink" title="如何监控和分析内存泄漏问题"></a>如何监控和分析内存泄漏问题</h2><p>上一个小结总结了上面是内存泄漏，是因为某些 GC ROOT 对象持有了期望释放的对象，导致期望释放的内存无法及时释放。所以如何监控和分析内存泄漏问题就成了如何找到 GC ROOT 的问题。</p>
<p>一般手动分析的步骤是：重复操作怀疑有内存泄漏的场景，然后触发几次 GC 。等几秒钟后，把 APP 的内存堆栈 dump 下来(可以使用 as 的工具 dump)，然后用 sdk 里面的 cover 工具转换一下，然后用 MAT 工具来分析内存泄漏的对象到 GC ROOT 的引用链。</p>
<p>手动分析总是很麻烦的，一个好消息是，有一个特别好用的自动监控和分析内存泄漏的工具，这个工具就是 leakcanary ，它可以自动监控并给出内存泄漏的对象到 GC ROOT 的引用链。</p>
<p>使用很简单，只需要在 APP  的 build.gradle 下面新增</p>
<pre><code>dependencies &#123;
  // debugImplementation because LeakCanary should only run in debug builds.
  debugImplementation &#39;com.squareup.leakcanary:leakcanary-android:2.7&#39;
&#125;
</code></pre>
<p>leakcanary 比较核心的一个原理就是利用了弱引用的一个特性，这个特性就是：</p>
<blockquote>
<p>在创建弱引用的时候，可以指定一个 RefrenceQueue ，当弱引用引用的对象的可达性发生变化的时候，系统会把这个弱引用引用的对象放到之前指定的 RefrenceQueue 中等待处理。</p>
</blockquote>
<p>所以 GC 后，引用对象仍然没有出现在 RefrenceQueue 的时候，说明可能发生了内存泄漏，这个时候 leakcanary 就会 dump 应用的 heap ，然后用 shark 库分析 heap ，找出一个到 GC ROOT 的最短引用链并提示。</p>
<h2 id="常见的内存泄漏的场景"><a href="#常见的内存泄漏的场景" class="headerlink" title="常见的内存泄漏的场景"></a>常见的内存泄漏的场景</h2><p>个人总结了下工作中碰到内存泄漏的一些场景，现记录下来，大家可以参考下。</p>
<ol>
<li>静态变量持有 Context 等。</li>
<li>单例实例持有 Context 等。</li>
<li>一些回调没有反注册，比如广播的注册和反注册等，有时候一些第三方库也需要注意。</li>
<li>一些 Listener 没有手动断开连接。</li>
<li>匿名内部类持有外部类的实例。比如 Handler , Runnable 等常见的用匿名内部类的实现，常常会不小心持有 Context 等外部类实例。</li>
</ol>
<h1 id="操作流畅度优化"><a href="#操作流畅度优化" class="headerlink" title="操作流畅度优化"></a>操作流畅度优化</h1><h2 id="为什么会卡顿"><a href="#为什么会卡顿" class="headerlink" title="为什么会卡顿"></a>为什么会卡顿</h2><p>为什么卡顿之前，我们先需要简单了解一点硬件相关的知识。就是在界面绘制的过程中， CPU 主要的任务是计算出屏幕上所有 View 对应的图形和向量等信息。 GPU 的主要任务就是把 CPU 计算出的图形栅格化并转化为位图，可以简单理解为屏幕像素点对应的值。</p>
<p>如果操作过程中卡顿了，一般就是 CPU 和 GPU 其中的一个或者多个无法短时间完成对应的任务。</p>
<p>一般而言，CPU 除了需要计算 View 对应的图形和向量等信息，还要做逻辑运算和文件读写等任务，所以 CPU 造成卡顿更常见。一般也是通过减少 CPU 的计算任务来优化卡顿。</p>
<p>影响 CPU 的使用率一般有以下几个方面：</p>
<ul>
<li>读写文件</li>
<li>解析大量图片</li>
<li>频繁请求网络</li>
<li>复杂的布局</li>
<li>频繁创建对象 </li>
</ul>
<h2 id="如何检测卡顿"><a href="#如何检测卡顿" class="headerlink" title="如何检测卡顿"></a>如何检测卡顿</h2><p>虽然我们知道了大概哪些原因会导致卡顿，但是我们无法准确定位出问题的代码点在哪里，针对上面的部分问题，本人写了一个开源库来自动检测，这个开源库的地址是</p>
<blockquote>
<p><a target="_blank" rel="noopener" href="https://github.com/XanderWang/performance">https://github.com/XanderWang/performance</a></p>
</blockquote>
<p>详细的原理，可以参考上面的连接，这里简单总结下监控 UI 卡段的原理。</p>
<p>我们知道，<code>Android</code> 里面，界面的刷新需要再主线程或者说 UI 线程执行。而界面的绘制起始点又利用了 <code>Looper 消息循环</code>机制。<code>Looper 消息循环</code>机制有一个有意思的特点，就是 Looper 在 <code>dispatch</code> Message 的时候，会在 <code>dispatch 前</code>和 <code>dispatch 后</code>利用 <code>Printer</code> 打印<code>特定 tag</code> 的字符串，通过<code>接管 Printer</code> ，我们就可以获取 <code>dispatch message</code> 前后的时机。</p>
<p>然后我们可以在 <code>dispatch message 之前</code>，在<code>异步线程</code>启动一个抓取系统信息的延时任务。在 <code>dispatch message 之后</code>，我们可以<code>移除</code>异步线程的这个延时任务。如果某个消息的执行没有超过阈值，那就表示在异步线程的延时任务被取消，表明没有卡顿。如果某个消息的执行时间超过了阈值，那异步线程里的延时任务就会执行，表明有卡顿，异步线程的延时任务会获取此时的系统状态，从而辅助我们分析卡顿问题。</p>
<h2 id="如何优化卡顿"><a href="#如何优化卡顿" class="headerlink" title="如何优化卡顿"></a>如何优化卡顿</h2><p>如何检测说完了，我们来说说如何优化。在 为什么会卡顿 小结我总结了几种常见，现在对几种场景的优化总结下。</p>
<h3 id="读写文件"><a href="#读写文件" class="headerlink" title="读写文件"></a>读写文件</h3><p>最常见的一个读写文件而不自知的就是 <code>SharePerfrences</code> 的使用，使用 <code>sp</code> 的时候需要注意不要频繁调用 <code>apply</code> 或者 <code>commit</code> 方法，因为每调用一次就有可能会有一次写文件操作(高版本系统做了优化 apply 做了优化，不一定会写文件)。所以，如果调用次数多的话，就会多次写文件，写文件又是一个耗时且耗资源的操作，所以要少做。</p>
<p>一般优化方法是合理<code>拆分</code> sp 文件，一个 sp 文件不要包含太多的项，同时每一项的内容尽量短。尽量批量提交数据后再 commit 或者 apply 。同时需要注意的是 commit 会直接触发写文件(内容有变化的时候)，所以如果在 UI 线程调用 commit 方法需要注意可能会阻塞 UI 线程。</p>
<p>如果有更高的性能需求，可以考虑用 <a target="_blank" rel="noopener" href="https://github.com/Tencent/MMKV">mmkv</a> 来替换或者 <a target="_blank" rel="noopener" href="https://developer.android.google.cn/topic/libraries/architecture/datastore?hl=zh-cn">DataStore</a> 来替换 sp 。具体的替换方法就不细说了。网上有很多资料参考。</p>
<p>另外一个常见的读写文件的场景是从 xml 文件里面读取布局、色值等操作，这些都是一些 io 操作。从 xml 读取布局的话，可以考虑用代码直接创建 view 来优化，从 xml 里面读取颜色可以考虑加个 HashMap 来优化。</p>
<h3 id="解析大量图片"><a href="#解析大量图片" class="headerlink" title="解析大量图片"></a>解析大量图片</h3><p>解码图片毫无疑问是一个计算量大的操作，所以一般加载图片的时候最好根据实际显示的尺寸做压缩，并且保存压缩后的缩略图，方便下次直接加载。</p>
<p>另外还需要注意列表滚动过程中，控制对图片的加载，一般列表在滑动过程中，不加载图片，等列表滚动停止后，才开始加载图片。</p>
<p>另外的一个优化的方法就是减少图片的使用，不过这个难度有点大。</p>
<p>另外还可以考虑针对不同的图片格式，用不同的解码格式。比如 <code>png</code> 格式的图片根据机器实际情况选择 <code>8888</code> 或者 <code>4444</code> 解码方式解码图片。如果是 <code>jpg/jpeg</code> 格式的图片，就用 <code>565</code> 的解码方式解码图片。对于用不同的解码方式解码图片，效率是否会高，本人没做过测试，但是毫无疑问，内存的使用是不同的。</p>
<h3 id="频繁请求网络"><a href="#频繁请求网络" class="headerlink" title="频繁请求网络"></a>频繁请求网络</h3><p>网络请求的话，可以参考下面的优化方法。</p>
<ol>
<li><p>如果使用 <code>okhttp</code> 请求网络的话，尽量全局使用一个 <code>httpclient</code> ，这样做的好处是可以复用，提高网络请求效率。</p>
</li>
<li><p>后台支持的话，开启 <code>gzip</code> 压缩，这样网络传输的数据量小些，传输效率会高些。</p>
</li>
<li><p>自定义 <code>dns</code> ，减少解析 <code>dns</code> 的时间。</p>
</li>
<li><p>通过和后台商量，部分数据后台接口一步到位，尽量避免多次请求后才拿到完整的目标数据。</p>
</li>
</ol>
<h3 id="复杂的布局"><a href="#复杂的布局" class="headerlink" title="复杂的布局"></a>复杂的布局</h3><p>如果布局复杂的话， CPU 要进行大量的计算才可以确定最终的图形。所以布局复杂的话，CPU 需要大量的运算资源，所以优化复杂的布局是很有必要的。</p>
<ol>
<li><p>减少布局层次，可以利用 ViewStub 、merge 和 include 等标签来尝试减少布局层次。</p>
</li>
<li><p>使用高效的布局容器，比如 ConstraintLayout，可以不用嵌套布局容器来实现复杂效果。</p>
</li>
<li><p>部分效果可以考虑用自定义 View 实现。</p>
</li>
</ol>
<p>这个优化感觉不是特别好做，可能优化了，但是效果不好，但是又不能不做。</p>
<h3 id="频繁创建对象"><a href="#频繁创建对象" class="headerlink" title="频繁创建对象"></a>频繁创建对象</h3><p>为什么这个要列出来呢？因为频繁创建对象，可能会短时间内消耗大量内存，然后内存不足的时候系统就会尝试 GC 来回收对象，而 GC 是很耗资源的操作，虽然现在 Android 系统对 GC 做了很多优化，但是尽量减少 GC 的触发总是好的。</p>
<p>一般频繁创建对象的场景有:</p>
<ul>
<li>自定义 View 的时候，在 onDraw 方法创建临时对象</li>
<li>循环里面使用 “+” 拼接字符串</li>
<li>ArrayList 等有容积限制的容器类初始化的容量不合理，导致后续新增数据频繁扩容。</li>
</ul>
<p>可能还有一些场景没有列出来，如果大家有好的建议，可以提出来。</p>
<p>除了频繁创建对象可能会触发 GC ，如果某次使用过大的内存也可能会导致 GC ，比如展示一个超大的 Bitmap ，虽然可以用缩略图来展示，但是可能会碰到需要放大查看具体细节的场景，这个时候可以考虑采用裁剪显示区域(BitmapRegionDecoder)的方式来解析图片。</p>
<h1 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h1><p>以上是我工作中涉及到的优化点，可能不是很全，也难免有遗漏。如果有错误和遗漏，还请大家指正。</p>
<h1 id="联系我"><a href="#联系我" class="headerlink" title="联系我"></a>联系我</h1><ul>
<li><p>Github: <a target="_blank" rel="noopener" href="https://github.com/XanderWang">https://github.com/XanderWang</a></p>
</li>
<li><p>Mail: <a href="mailto:&#x34;&#50;&#48;&#x36;&#52;&#x30;&#55;&#54;&#x33;&#64;&#113;&#113;&#x2e;&#99;&#111;&#109;">&#x34;&#50;&#48;&#x36;&#52;&#x30;&#55;&#54;&#x33;&#64;&#113;&#113;&#x2e;&#99;&#111;&#109;</a></p>
</li>
<li><p>Blog: <a href="https://xander_wang.gitee.io/android-note/">https://xander_wang.gitee.io/android-note/</a></p>
</li>
</ul>

  
  
    
    <div class='footer'>
      
      
      
        <div class='copyright'>
          <blockquote>
            
              
                <p>本文永久链接是：<a href=http://xander_wang.gitee.io/android-note/2021/04/13/android_performance/android_performance_all_in_one/>http://xander_wang.gitee.io/android-note/2021/04/13/android_performance/android_performance_all_in_one/</a></p>
              
            
          </blockquote>
        </div>
      
      
    </div>
  
  
    


  <div class='article-meta' id="bottom">
    <div class='new-meta-box'>
      
        
          
<div class='new-meta-item author'>
  <a class='author' target="_blank" href="https://github.com/XanderWang/" rel="nofollow noopener">
    <img no-lazy src="https://i.loli.net/2020/05/18/WO1oJwFsRb4gV79.png">
    <p>XanderWang</p>
  </a>
</div>

        
      
        
          <div class="new-meta-item date" itemprop="dateUpdated" datetime="2022-12-11T10:26:22+00:00">
  <a class='notlink'>
    <i class="fas fa-edit fa-fw" aria-hidden="true"></i>
    <p>更新于：Dec 11, 2022</p>
  </a>
</div>

        
      
        
          
  
  <div class="new-meta-item meta-tags"><a class="tag" href="/android-note/tags/Android/" rel="nofollow"><i class="fas fa-hashtag fa-fw" aria-hidden="true"></i><p>Android</p></a></div> <div class="new-meta-item meta-tags"><a class="tag" href="/android-note/tags/Performance/" rel="nofollow"><i class="fas fa-hashtag fa-fw" aria-hidden="true"></i><p>Performance</p></a></div>


        
      
        
          
  <div class="new-meta-item share -mob-share-list">
  <div class="-mob-share-list share-body">
    
      
        <a class="-mob-share-qq" title="" rel="external nofollow noopener noreferrer noopener"
          
          target="_blank" href="http://connect.qq.com/widget/shareqq/index.html?url=http://xander_wang.gitee.io/android-note/2021/04/13/android_performance/android_performance_all_in_one/&title=Android 性能优化总结 - Xander's blog&summary="
          
          >
          
            <img src="https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/logo/128/qq.png" class="lazyload placeholder" data-srcset="https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/logo/128/qq.png" srcset="https://cdn.jsdelivr.net/gh/volantis-x/cdn-volantis@3/img/placeholder/c617bfd2497fcea598e621413e315c368f8d8e.svg">
          
        </a>
      
    
      
        <a class="-mob-share-qzone" title="" rel="external nofollow noopener noreferrer noopener"
          
          target="_blank" href="https://sns.qzone.qq.com/cgi-bin/qzshare/cgi_qzshare_onekey?url=http://xander_wang.gitee.io/android-note/2021/04/13/android_performance/android_performance_all_in_one/&title=Android 性能优化总结 - Xander's blog&summary="
          
          >
          
            <img src="https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/logo/128/qzone.png" class="lazyload placeholder" data-srcset="https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/logo/128/qzone.png" srcset="https://cdn.jsdelivr.net/gh/volantis-x/cdn-volantis@3/img/placeholder/c617bfd2497fcea598e621413e315c368f8d8e.svg">
          
        </a>
      
    
      
        <a class="-mob-share-weibo" title="" rel="external nofollow noopener noreferrer noopener"
          
          target="_blank" href="http://service.weibo.com/share/share.php?url=http://xander_wang.gitee.io/android-note/2021/04/13/android_performance/android_performance_all_in_one/&title=Android 性能优化总结 - Xander's blog&summary="
          
          >
          
            <img src="https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/logo/128/weibo.png" class="lazyload placeholder" data-srcset="https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/logo/128/weibo.png" srcset="https://cdn.jsdelivr.net/gh/volantis-x/cdn-volantis@3/img/placeholder/c617bfd2497fcea598e621413e315c368f8d8e.svg">
          
        </a>
      
    
      
        
        <div class='hoverbox'>
          <a class='share'><img src="https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/logo/128/wechat.png" class="lazyload placeholder" data-srcset="https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/logo/128/wechat.png" srcset="https://cdn.jsdelivr.net/gh/volantis-x/cdn-volantis@3/img/placeholder/c617bfd2497fcea598e621413e315c368f8d8e.svg"></a>
          <div class='target'>
            <img src="">
          </div>
        </div>
      
    
      
    
  </div>
</div>



        
      
    </div>
  </div>


  
  

  
    <div class="prev-next">
      
        <a class='prev' href='/android-note/2021/04/24/android_architecture/android_architecture_01/'>
          <p class='title'><i class="fas fa-chevron-left" aria-hidden="true"></i>Android 架构设计探索</p>
          <p class='content'>什么是架构
如何做架构
</p>
        </a>
      
      
        <a class='next' href='/android-note/2021/04/10/android_performance/android_performance_app_caton/'>
          <p class='title'>Android 性能优化 - 操作流畅度优化<i class="fas fa-chevron-right" aria-hidden="true"></i></p>
          <p class='content'>系列文章
Android 性能优化概要
APK 瘦身优化
启动速度优化
稳定性优化
内存的优化
操作流畅度优化

为什么会卡顿为什么卡顿之前，我们先需要简单了解一点硬件相关的知识。就是在界面绘制...</p>
        </a>
      
    </div>
  
</article>


  

  <article class="post white-box reveal shadow" id="comments">
    <p ct><i class='fas fa-comments'></i> 评论</p>
    
    <div id="valine_container" class="valine_thread">
  <i class="fas fa-cog fa-spin fa-fw fa-2x"></i>
</div>

  </article>






</div>
<aside class='l_side'>
  
  
    
    



  <section class="widget toc-wrapper shadow desktop mobile" id="toc-div" >
    
  <header>
    
      <i class="fas fa-list fa-fw" aria-hidden="true"></i><span class='name'>本文目录</span>
    
  </header>


    <div class='content'>
        <ol class="toc"><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%A6%82%E4%BD%95%E6%9F%A5%E7%9C%8B-APK-%E7%9A%84%E7%BB%84%E6%88%90"><span class="toc-text">如何查看 APK 的组成</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%A6%82%E4%BD%95%E5%87%8F%E5%B0%91-res-%E8%B5%84%E6%BA%90%E5%A4%A7%E5%B0%8F"><span class="toc-text">如何减少 res 资源大小</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%A6%82%E4%BD%95%E5%87%8F%E5%B0%91-so-%E5%BA%93%E8%B5%84%E6%BA%90%E5%A4%A7%E5%B0%8F"><span class="toc-text">如何减少 so 库资源大小</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%A6%82%E4%BD%95%E5%87%8F%E5%B0%91%E4%BB%A3%E7%A0%81%E8%B5%84%E6%BA%90%E5%A4%A7%E5%B0%8F"><span class="toc-text">如何减少代码资源大小</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%8F%82%E8%80%83%E6%96%87%E6%A1%A3"><span class="toc-text">参考文档</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%90%AF%E5%8A%A8%E7%9A%84%E7%B1%BB%E5%9E%8B"><span class="toc-text">启动的类型</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96%E5%90%AF%E5%8A%A8%E6%97%B6%E9%97%B4"><span class="toc-text">如何获取启动时间</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%A6%82%E4%BD%95%E7%9B%91%E6%8E%A7%E5%90%AF%E5%8A%A8%E8%BF%87%E7%A8%8B"><span class="toc-text">如何监控启动过程</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E4%B8%80%E8%88%AC%E6%9C%89%E9%82%A3%E4%BA%9B%E4%BC%98%E5%8C%96%E6%96%B9%E6%B3%95"><span class="toc-text">一般有那些优化方法</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#APP-%E7%A8%B3%E5%AE%9A%E6%80%A7%E7%9A%84%E7%BB%B4%E5%BA%A6"><span class="toc-text">APP 稳定性的维度</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%A6%82%E4%BD%95%E6%B2%BB%E7%90%86-Crash"><span class="toc-text">如何治理 Crash</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#Java-%E5%B1%82%E8%8E%B7%E5%8F%96%E6%9C%AA%E5%A4%84%E7%90%86%E7%9A%84%E5%BC%82%E5%B8%B8%E7%9A%84%E8%B0%83%E7%94%A8%E5%A0%86%E6%A0%88"><span class="toc-text">Java 层获取未处理的异常的调用堆栈</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#Native-%E5%B1%82%E8%8E%B7%E5%8F%96%E6%9C%AA%E5%A4%84%E7%90%86%E7%9A%84%E5%BC%82%E5%B8%B8%E7%9A%84%E7%9B%B8%E5%85%B3%E4%BF%A1%E6%81%AF"><span class="toc-text">Native 层获取未处理的异常的相关信息</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%A6%82%E4%BD%95%E6%B2%BB%E7%90%86-ANR"><span class="toc-text">如何治理 ANR</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%86%85%E5%AD%98%E9%83%BD%E6%B6%88%E8%80%97%E5%9C%A8%E5%93%AA%E9%87%8C%E4%BA%86"><span class="toc-text">内存都消耗在哪里了</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%A6%82%E4%BD%95%E5%90%88%E7%90%86%E4%BD%BF%E7%94%A8%E5%86%85%E5%AD%98"><span class="toc-text">如何合理使用内存</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E6%98%AF%E4%BB%80%E4%B9%88"><span class="toc-text">内存泄漏是什么</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%A6%82%E4%BD%95%E7%9B%91%E6%8E%A7%E5%92%8C%E5%88%86%E6%9E%90%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E9%97%AE%E9%A2%98"><span class="toc-text">如何监控和分析内存泄漏问题</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%B8%B8%E8%A7%81%E7%9A%84%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E7%9A%84%E5%9C%BA%E6%99%AF"><span class="toc-text">常见的内存泄漏的场景</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BC%9A%E5%8D%A1%E9%A1%BF"><span class="toc-text">为什么会卡顿</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%A6%82%E4%BD%95%E6%A3%80%E6%B5%8B%E5%8D%A1%E9%A1%BF"><span class="toc-text">如何检测卡顿</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%A6%82%E4%BD%95%E4%BC%98%E5%8C%96%E5%8D%A1%E9%A1%BF"><span class="toc-text">如何优化卡顿</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E8%AF%BB%E5%86%99%E6%96%87%E4%BB%B6"><span class="toc-text">读写文件</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E8%A7%A3%E6%9E%90%E5%A4%A7%E9%87%8F%E5%9B%BE%E7%89%87"><span class="toc-text">解析大量图片</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E9%A2%91%E7%B9%81%E8%AF%B7%E6%B1%82%E7%BD%91%E7%BB%9C"><span class="toc-text">频繁请求网络</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E5%A4%8D%E6%9D%82%E7%9A%84%E5%B8%83%E5%B1%80"><span class="toc-text">复杂的布局</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E9%A2%91%E7%B9%81%E5%88%9B%E5%BB%BA%E5%AF%B9%E8%B1%A1"><span class="toc-text">频繁创建对象</span></a></li></ol></li></ol>
    </div>
  </section>


  


</aside>



		  
		  <!--此文件用来存放一些不方便取值的变量-->
<!--思路大概是将值藏到重加载的区域内-->

<script>
  window.pdata={}
  pdata.ispage=true;
  pdata.postTitle="Android 性能优化总结";
  pdata.commentPath="";
  pdata.commentPlaceholder="";
  // header 这里无论是否开启pjax都需要
  var l_header=document.getElementById("l_header");
  
  l_header.classList.add("show");
  
  
    // cover
    var cover_wrapper=document.querySelector('.cover-wrapper');
    
    cover_wrapper.id="none";
    cover_wrapper.style.display="none";
    
  
</script>

        </div>
        
  
  <footer class="footer clearfix">
    <br><br>
    
      
        <div><p>Blog content follows the <a target="_blank" rel="noopener" href="https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en">Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) License</a></p>
</div>
      
    
      
        
          <div><p><span id="lc-sv">本站总访问量为 <span id='number'><i class="fas fa-circle-notch fa-spin fa-fw" aria-hidden="true"></i></span> 次</span> <span id="lc-uv">访客数为 <span id='number'><i class="fas fa-circle-notch fa-spin fa-fw" aria-hidden="true"></i></span> 人</span></p>
</div>
        
      
    
      
        Use
        <a href="https://github.com/volantis-x/hexo-theme-volantis/tree/4.3.1" target="_blank" class="codename">Volantis</a>
        as theme, total visits
          <span id="busuanzi_value_site_pv"><i class="fas fa-circle-notch fa-spin fa-fw" aria-hidden="true"></i></span>
          times
        
      
    
      
        <div class='copyright'>
        <p><a href="/">Copyright © 2017-2020 XXX</a></p>

        </div>
      
    
      
        
      
    
  </footer>


        <a id="s-top" class="fas fa-arrow-up fa-fw" href="javascript:void(0)"></a>
      </div>
    </div>
    <div>
      <script>
/************这个文件存放不需要重载的全局变量和全局函数*********/
window.volantis={};
window.volantis.loadcss=document.getElementById("loadcss");
/******************** Pjax ********************************/
function VPjax(){
	this.list=[] // 存放回调函数
	this.start=()=>{
	  for(var i=0;i<this.list.length;i++){
		this.list[i].run();
	  }
	}
	this.push=(fn,name)=>{
		var f=new PjaxItem(fn,name);
		this.list.push(f);
	}
	// 构造一个可以run的对象
	function PjaxItem(fn,name){
		// 函数名称
		this.name = name || fn.name
		// run方法
		this.run=()=>{
			fn()
		}
	}
}
volantis.pjax={}
volantis.pjax.method={
	complete: new VPjax(),
	error: new VPjax(),
	send: new VPjax()
}
volantis.pjax={
	...volantis.pjax,
	push: volantis.pjax.method.complete.push,
	error: volantis.pjax.method.error.push,
	send: volantis.pjax.method.send.push
}
/********************脚本懒加载函数********************************/
// 已经加入了setTimeout
function loadScript(src, cb) {
	setTimeout(function() {
		var HEAD = document.getElementsByTagName('head')[0] || document.documentElement;
		var script = document.createElement('script');
		script.setAttribute('type','text/javascript');
		if (cb) script.onload = cb;
		script.setAttribute('src', src);
		HEAD.appendChild(script);
	});
}
//https://github.com/filamentgroup/loadCSS
var loadCSS = function( href, before, media, attributes ){
	var doc = window.document;
	var ss = doc.createElement( "link" );
	var ref;
	if( before ){
		ref = before;
	}
	else {
		var refs = ( doc.body || doc.getElementsByTagName( "head" )[ 0 ] ).childNodes;
		ref = refs[ refs.length - 1];
	}
	var sheets = doc.styleSheets;
	if( attributes ){
		for( var attributeName in attributes ){
			if( attributes.hasOwnProperty( attributeName ) ){
				ss.setAttribute( attributeName, attributes[attributeName] );
			}
		}
	}
	ss.rel = "stylesheet";
	ss.href = href;
	ss.media = "only x";
	function ready( cb ){
		if( doc.body ){
			return cb();
		}
		setTimeout(function(){
			ready( cb );
		});
	}
	ready( function(){
		ref.parentNode.insertBefore( ss, ( before ? ref : ref.nextSibling ) );
	});
	var onloadcssdefined = function( cb ){
		var resolvedHref = ss.href;
		var i = sheets.length;
		while( i-- ){
			if( sheets[ i ].href === resolvedHref ){
				return cb();
			}
		}
		setTimeout(function() {
			onloadcssdefined( cb );
		});
	};
	function loadCB(){
		if( ss.addEventListener ){
			ss.removeEventListener( "load", loadCB );
		}
		ss.media = media || "all";
	}
	if( ss.addEventListener ){
		ss.addEventListener( "load", loadCB);
	}
	ss.onloadcssdefined = onloadcssdefined;
	onloadcssdefined( loadCB );
	return ss;
};
</script>
<script>
  
  loadCSS("https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.14/css/all.min.css", window.volantis.loadcss);
  
  
  loadCSS("https://cdn.jsdelivr.net/gh/l-lin/font-awesome-animation/dist/font-awesome-animation.min.css", window.volantis.loadcss);
  
  
  loadCSS("https://cdn.jsdelivr.net/npm/node-waves@0.7.6/dist/waves.min.css", window.volantis.loadcss);
  
  
</script>
<!-- required -->

<script src="https://cdn.jsdelivr.net/npm/jquery@3.5/dist/jquery.min.js"></script>

<script>
  function pjax_fancybox() {
    $(".md .gallery").find("img").each(function () { //渲染 fancybox
      var element = document.createElement("a"); // a 标签
      $(element).attr("class", "fancybox");
      $(element).attr("pjax-fancybox", "");  // 过滤 pjax
      $(element).attr("href", $(this).attr("src"));
      if ($(this).attr("data-original")) {
        $(element).attr("href", $(this).attr("data-original"));
      }
      $(element).attr("data-fancybox", "images");
      var caption = "";   // 描述信息
      if ($(this).attr('alt')) {  // 判断当前页面是否存在描述信息
        $(element).attr('data-caption', $(this).attr('alt'));
        caption = $(this).attr('alt');
      }
      var div = document.createElement("div");
      $(div).addClass("fancybox");
      $(this).wrap(div); // 最外层套 div ，其实主要作用还是 class 样式
      var span = document.createElement("span");
      $(span).addClass("image-caption");
      $(span).text(caption); // 加描述
      $(this).after(span);  // 再套一层描述
      $(this).wrap(element);  // 最后套 a 标签
    })
    $(".md .gallery").find("img").fancybox({
      selector: '[data-fancybox="images"]',
      hash: false,
      loop: false,
      closeClick: true,
      helpers: {
        overlay: {closeClick: true}
      },
      buttons: [
        "zoom",
        "close"
      ]
    });
  };
  function SCload_fancybox() {
    if ($(".md .gallery").find("img").length == 0) return;
    loadCSS("https://cdn.jsdelivr.net/npm/@fancyapps/fancybox@3.5.7/dist/jquery.fancybox.min.css", document.getElementById("loadcss"));
    loadScript('https://cdn.jsdelivr.net/gh/fancyapps/fancybox@3.5.7/dist/jquery.fancybox.min.js', pjax_fancybox)
  };
  $(function () {
    SCload_fancybox();
  });
  function Pjax_SCload_fancybox(){
	if (typeof $.fancybox == "undefined") {
	 SCload_fancybox();
    } else {
	 pjax_fancybox();
    }
  }
  volantis.pjax.push(Pjax_SCload_fancybox)
  volantis.pjax.send(()=>{
      if (typeof $.fancybox != "undefined") {
        $.fancybox.close();    // 关闭弹窗
      }
  },'fancybox')
</script>


<!-- internal -->

  
  
  
    
<script src="https://cdn.jsdelivr.net/npm/jquery-backstretch@2.1.18/jquery.backstretch.min.js"></script>

    <script type="text/javascript">
        var imgs=["https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/001.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/002.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/003.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/004.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/005.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/006.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/012.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/016.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/019.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/025.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/033.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/034.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/035.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/038.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/039.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/042.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/046.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/051.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/052.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/054.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/056.jpg"];
        if ('true' == 'true') {
          function shuffle(arr){
            /*From countercurrent-time*/
            var n = arr.length;
            while(n--) {
              var index = Math.floor(Math.random() * n);
              var temp = arr[index];
              arr[index] = arr[n];
              arr[n] = temp;
            }
          }
          shuffle(imgs);
        }
	  function Pjax_backstretch(){
        
          $('#cover-backstretch').backstretch(
            imgs,
          {
            duration: "10000",
            fade: "1500"
          });
        
	  }
	  loadScript("https://cdn.jsdelivr.net/npm/jquery-backstretch@2.1.18/jquery.backstretch.min.js",Pjax_backstretch)
    </script>
  




<script>
  function loadIssuesJS() {
    if ($(".md").find(".issues-api").length == 0) return;
	
	  loadScript('/android-note/js/issues.js');
	
  };
  $(function () {
    loadIssuesJS();
  });
  volantis.pjax.push(()=>{
	if (typeof IssuesAPI == "undefined") {
	  loadIssuesJS();
	}
  },"IssuesJS")
</script>



  <script defer src="https://cdn.jsdelivr.net/npm/vanilla-lazyload@17.1.0/dist/lazyload.min.js"></script>
<script>
  // https://www.npmjs.com/package/vanilla-lazyload
  // Set the options globally
  // to make LazyLoad self-initialize
  window.lazyLoadOptions = {
    elements_selector: ".lazyload",
    threshold: 0
  };
  // Listen to the initialization event
  // and get the instance of LazyLoad
  window.addEventListener(
    "LazyLoad::Initialized",
    function (event) {
      window.lazyLoadInstance = event.detail.instance;
    },
    false
  );
  document.addEventListener('DOMContentLoaded', function () {
    lazyLoadInstance.update();
  });
  document.addEventListener('pjax:complete', function () {
    lazyLoadInstance.update();
  });
</script>




  

<script>
  window.FPConfig = {
	delay: 0,
	ignoreKeywords: [],
	maxRPS: 5,
	hoverDelay: 25
  };
</script>
<script defer src="https://cdn.jsdelivr.net/gh/gijo-varghese/flying-pages@2.1.2/flying-pages.min.js"></script>





  <script src="https://cdn.jsdelivr.net/npm/clipboard@2/dist/clipboard.min.js"></script>
<script>
    var clipboard = new ClipboardJS('.btn-copy', {
        target: function (trigger) {
            return trigger.nextElementSibling
        }
    });
    function wait(callback, seconds) {
        var timelag = null;
        timelag = window.setTimeout(callback, seconds)
    }
    function pjax_initCopyCode() {
		if($(".highlight .code pre").length+$(".article pre code").length==0)return;
        var copyHtml = '';
        copyHtml += '<button class="btn-copy" data-clipboard-snippet="">';
        copyHtml += '<i class="fas fa-copy"></i><span>COPY</span>';
        copyHtml += '</button>';
        $(".highlight .code pre").before(copyHtml);
        $(".article pre code").before(copyHtml);
        clipboard.off('success').on('success', function (e) {
            let $btn = $(e.trigger);
            $btn.addClass('copied');
            let $icon = $($btn.find('i'));
            $icon.removeClass('fa-copy');
            $icon.addClass('fa-check-circle');
            let $span = $($btn.find('span'));
            $span[0].innerText = 'COPIED';
            wait(function () {
                $icon.removeClass('fa-check-circle');
                $icon.addClass('fa-copy');
                $span[0].innerText = 'COPY'
            }, 2000)
        });
        clipboard.off('error').on('error', function (e) {
            e.clearSelection();
            let $btn = $(e.trigger);
            $btn.addClass('copy-failed');
            let $icon = $($btn.find('i'));
            $icon.removeClass('fa-copy');
            $icon.addClass('fa-times-circle');
            let $span = $($btn.find('span'));
            $span[0].innerText = 'COPY FAILED';
            wait(function () {
                $icon.removeClass('fa-times-circle');
                $icon.addClass('fa-copy');
                $span[0].innerText = 'COPY'
            }, 2000)
        })
    }
    $(function () {
        pjax_initCopyCode()
    });
	volantis.pjax.push(pjax_initCopyCode)
</script>




   <script type="text/javascript">
  loadScript("https://cdn.jsdelivr.net/npm/scrollreveal@4.0.6/dist/scrollreveal.min.js")
  function pjax_scrollrebeal() {
    ScrollReveal().reveal('.l_main .reveal', {
      distance: '32px',
      duration: '800',
      interval: '20',
      scale: '1',
      easing: 'ease-out'
    });
  }
  $(function () {
  var checkScrollReveal = setInterval(function () {
    if ($("#safearea").css("display")!="block") return
    if (typeof ScrollReveal=="undefined") return
    clearInterval(checkScrollReveal)
	pjax_scrollrebeal();
  }, 100)
  });
  volantis.pjax.push(pjax_scrollrebeal)
</script>






  
  
<script src="https://cdn.jsdelivr.net/npm/valine@1.4/dist/Valine.min.js"></script>


<script>
  function emoji(path, idx, ext) {
    return path + "/" + path + "-" + idx + "." + ext;
  }
  var emojiMaps = {};
  for (var i = 1; i <= 54; i++) {
    emojiMaps['tieba-' + i] = emoji('tieba', i, 'png');
  }
  for (var i = 1; i <= 101; i++) {
    emojiMaps['qq-' + i] = emoji('qq', i, 'gif');
  }
  for (var i = 1; i <= 116; i++) {
    emojiMaps['aru-' + i] = emoji('aru', i, 'gif');
  }
  for (var i = 1; i <= 125; i++) {
    emojiMaps['twemoji-' + i] = emoji('twemoji', i, 'png');
  }
  for (var i = 1; i <= 4; i++) {
    emojiMaps['weibo-' + i] = emoji('weibo', i, 'png');
  }
  function pjax_valine() {
    if(!document.querySelectorAll("#valine_container")[0])return;
    let pagePlaceholder = pdata.commentPlaceholder || "快来评论吧~";
    let path = pdata.commentPath;
    if (path.length == 0) {
      let defaultPath = '';
      path = defaultPath || decodeURI(window.location.pathname);
    }
    var valine = new Valine();
    valine.init(Object.assign({"path":null,"placeholder":"快来评论吧~","appId":"rFjxhNkjfJEFwCxatct9lFP5-gzGzoHsz","appKey":"bPYEsXC56n2ziDYGE5pg7EHW","meta":["nick","mail"],"requiredFields":["nick"],"enableQQ":true,"recordIP":false,"avatar":"robohash","pageSize":10,"lang":"zh-cn","highlight":true,"mathJax":false,"js":"https://cdn.jsdelivr.net/npm/valine@1.4/dist/Valine.min.js"}, {
      el: '#valine_container',
      path: path,
      placeholder: pagePlaceholder,
      emojiCDN: 'https://cdn.jsdelivr.net/gh/volantis-x/cdn-emoji/valine/',
      emojiMaps: emojiMaps,
    }))
  }
  $(function () {
    pjax_valine();
  });
  volantis.pjax.push(pjax_valine);
</script>




  <script defer src="https://cdn.jsdelivr.net/gh/volantis-x/cdn-busuanzi@2.3/js/busuanzi.pure.mini.js" data-pjax></script>



  
<script src="/android-note/js/app.js"></script>



<!-- optional -->

  <script>
const SearchServiceimagePath="https://cdn.jsdelivr.net/gh/volantis-x/cdn-volantis@master/img/";
const ROOT =  ("/android-note/" || "/").endsWith('/') ? ("/android-note/" || "/") : ("/android-note//" || "/" );

$('.input.u-search-input').one('focus',function(){
	
		loadScript('/android-note/js/search/hexo.js',setSearchService);
	
})

function listenSearch(){
  
    customSearch = new HexoSearch({
      imagePath: SearchServiceimagePath
    });
  
}
function setSearchService() {
	listenSearch();
	
}
</script>





  
<script src="https://cdn.jsdelivr.net/npm/node-waves@0.7.6/dist/waves.min.js"></script>

<script type="text/javascript">
$(function () {
  Waves.attach('.flat-btn', ['waves-button']);
  Waves.attach('.float-btn', ['waves-button', 'waves-float']);
  Waves.attach('.float-btn-light', ['waves-button', 'waves-float', 'waves-light']);
  Waves.attach('.flat-box', ['waves-block']);
  Waves.attach('.float-box', ['waves-block', 'waves-float']);
  Waves.attach('.waves-image');
  Waves.init();
});
</script>



  
<script src="https://cdn.jsdelivr.net/gh/volantis-x/cdn-volantis@2/js/comment_typing.js"></script>






  <script defer>

  const LCCounter = {
    app_id: 'rFjxhNkjfJEFwCxatct9lFP5-gzGzoHsz',
    app_key: 'bPYEsXC56n2ziDYGE5pg7EHW',
    custom_api_server: '',

    // 查询存储的记录
    getRecord(Counter, url, title) {
      return new Promise(function (resolve, reject) {
        Counter('get', '/classes/Counter?where=' + encodeURIComponent(JSON.stringify({url})))
          .then(resp => resp.json())
          .then(({results, code, error}) => {
            if (code === 401) {
              throw error;
            }
            if (results && results.length > 0) {
              var record = results[0];
              resolve(record);
            } else {
              Counter('post', '/classes/Counter', {url, title: title, times: 0})
                .then(resp => resp.json())
                .then((record, error) => {
                  if (error) {
                    throw error;
                  }
                  resolve(record);
                }).catch(error => {
                console.error('Failed to create', error);
                reject(error);
              });
            }
          }).catch((error) => {
          console.error('LeanCloud Counter Error:', error);
          reject(error);
        });
      })
    },

    // 发起自增请求
    increment(Counter, incrArr) {
      return new Promise(function (resolve, reject) {
        Counter('post', '/batch', {
          "requests": incrArr
        }).then((res) => {
          res = res.json();
          if (res.error) {
            throw res.error;
          }
          resolve(res);
        }).catch((error) => {
          console.error('Failed to save visitor count', error);
          reject(error);
        });
      });
    },

    // 构建自增请求体
    buildIncrement(objectId) {
      return {
        "method": "PUT",
        "path": `/1.1/classes/Counter/${ objectId }`,
        "body": {
          "times": {
            '__op': 'Increment',
            'amount': 1
          }
        }
      }
    },

    // 校验是否为有效的 UV
    validUV() {
      var key = 'LeanCloudUVTimestamp';
      var flag = localStorage.getItem(key);
      if (flag) {
        // 距离标记小于 24 小时则不计为 UV
        if (new Date().getTime() - parseInt(flag) <= 86400000) {
          return false;
        }
      }
      localStorage.setItem(key, new Date().getTime().toString());
      return true;
    },

    addCount(Counter) {
      var enableIncr = '' === 'true' && window.location.hostname !== 'localhost';
      enableIncr = true;
      var getterArr = [];
      var incrArr = [];
      // 请求 PV 并自增
      var pvCtn = document.querySelector('#lc-sv');
      if (pvCtn || enableIncr) {
        var pvGetter = this.getRecord(Counter, 'http://xander_wang.gitee.io/android-note' + '/#lc-sv', 'Visits').then((record) => {
          incrArr.push(this.buildIncrement(record.objectId))
          var eles = document.querySelectorAll('#lc-sv #number');
          if (eles.length > 0) {
            eles.forEach((el,index,array)=>{
              el.innerText = record.times + 1;
              if (pvCtn) {
                pvCtn.style.display = 'inline';
              }
            })
          }
        });
        getterArr.push(pvGetter);
      }

      // 请求 UV 并自增
      var uvCtn = document.querySelector('#lc-uv');
      if (uvCtn || enableIncr) {
        var uvGetter = this.getRecord(Counter, 'http://xander_wang.gitee.io/android-note' + '/#lc-uv', 'Visitors').then((record) => {
          var vuv = this.validUV();
          vuv && incrArr.push(this.buildIncrement(record.objectId))
          var eles = document.querySelectorAll('#lc-uv #number');
          if (eles.length > 0) {
            eles.forEach((el,index,array)=>{
              el.innerText = record.times + (vuv ? 1 : 0);
              if (uvCtn) {
                uvCtn.style.display = 'inline';
              }
            })
          }
        });
        getterArr.push(uvGetter);
      }

      // 请求文章的浏览数，如果是当前页面就自增
      var allPV = document.querySelectorAll('#lc-pv');
      if (allPV.length > 0 || enableIncr) {
        for (i = 0; i < allPV.length; i++) {
          let pv = allPV[i];
          let title = pv.getAttribute('data-title');
          var url = 'http://xander_wang.gitee.io/android-note' + pv.getAttribute('data-path');
          if (url) {
            var viewGetter = this.getRecord(Counter, url, title).then((record) => {
              // 是当前页面就自增
              let curPath = window.location.pathname;
              if (curPath.includes('index.html')) {
                curPath = curPath.substring(0, curPath.lastIndexOf('index.html'));
              }
              if (pv.getAttribute('data-path') == curPath) {
                incrArr.push(this.buildIncrement(record.objectId));
              }
              if (pv) {
                var ele = pv.querySelector('#lc-pv #number');
                if (ele) {
                  if (pv.getAttribute('data-path') == curPath) {
                    ele.innerText = (record.times || 0) + 1;
                  } else {
                    ele.innerText = record.times || 0;
                  }
                  pv.style.display = 'inline';
                }
              }
            });
            getterArr.push(viewGetter);
          }
        }
      }

      // 如果启动计数自增，批量发起自增请求
      if (enableIncr) {
        Promise.all(getterArr).then(() => {
          incrArr.length > 0 && this.increment(Counter, incrArr);
        })
      }

    },


    fetchData(api_server) {
      var Counter = (method, url, data) => {
        return fetch(`${ api_server }/1.1${ url }`, {
          method,
          headers: {
            'X-LC-Id': this.app_id,
            'X-LC-Key': this.app_key,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify(data)
        });
      };
      this.addCount(Counter);
    },

    refreshCounter() {
      var api_server = this.app_id.slice(-9) !== '-MdYXbMMI' ? this.custom_api_server : `https://${ this.app_id.slice(0, 8).toLowerCase() }.api.lncldglobal.com`;
      if (api_server) {
        this.fetchData(api_server);
      } else {
        fetch('https://app-router.leancloud.cn/2/route?appId=' + this.app_id)
          .then(resp => resp.json())
          .then(({api_server}) => {
            this.fetchData('https://' + api_server);
          });
      }
    }

  };

  LCCounter.refreshCounter();

  document.addEventListener('pjax:complete', function () {
    LCCounter.refreshCounter();
  });
</script>




  <script>
const rootElement = document.documentElement;
const darkModeStorageKey = "user-color-scheme";
const rootElementDarkModeAttributeName = "data-user-color-scheme";

const setLS = (k, v) => {
    localStorage.setItem(k, v);
};

const removeLS = (k) => {
    localStorage.removeItem(k);
};

const getLS = (k) => {
    return localStorage.getItem(k);
};

const getModeFromCSSMediaQuery = () => {
  return window.matchMedia("(prefers-color-scheme: dark)").matches
    ? "dark"
    : "light";
};

const resetRootDarkModeAttributeAndLS = () => {
  rootElement.removeAttribute(rootElementDarkModeAttributeName);
  removeLS(darkModeStorageKey);
};

const validColorModeKeys = {
  dark: true,
  light: true,
};

const applyCustomDarkModeSettings = (mode) => {
  const currentSetting = mode || getLS(darkModeStorageKey);

  if (currentSetting === getModeFromCSSMediaQuery()) {
    resetRootDarkModeAttributeAndLS();
  } else if (validColorModeKeys[currentSetting]) {
    rootElement.setAttribute(rootElementDarkModeAttributeName, currentSetting);
  } else {
    resetRootDarkModeAttributeAndLS();
  }
};

const invertDarkModeObj = {
  dark: "light",
  light: "dark",
};

/**
 * get target mode
 */
const toggleCustomDarkMode = () => {
  let currentSetting = getLS(darkModeStorageKey);

  if (validColorModeKeys[currentSetting]) {
    currentSetting = invertDarkModeObj[currentSetting];
  } else if (currentSetting === null) {
    currentSetting = invertDarkModeObj[getModeFromCSSMediaQuery()];
  } else {
    return;
  }
  setLS(darkModeStorageKey, currentSetting);
  return currentSetting;
};

/**
 * bind click event for toggle button
 */
var btn=$("#wrapper .toggle-mode-btn,#rightmenu-wrapper .toggle-mode-btn");
function bindToggleButton() {
    btn.on('click',(e) => {
      const mode = toggleCustomDarkMode();
      applyCustomDarkModeSettings(mode);
    });
}

applyCustomDarkModeSettings();
document.addEventListener("DOMContentLoaded", bindToggleButton);
volantis.pjax.push(bindToggleButton);
volantis.pjax.send(()=>{
	btn.unbind('click');
},'toggle-mode-btn-unbind');
</script>








<script>
function listennSidebarTOC() {
  const navItems = document.querySelectorAll(".toc li");
  if (!navItems.length) return;
  const sections = [...navItems].map((element) => {
    const link = element.querySelector(".toc-link");
    const target = document.getElementById(
      decodeURI(link.getAttribute("href")).replace("#", "")
    );
    link.addEventListener("click", (event) => {
      event.preventDefault();
      window.scrollTo({
		top: target.offsetTop + 100,
		
		behavior: "smooth"
		
	  });
    });
    return target;
  });

  function activateNavByIndex(target) {
    if (target.classList.contains("active-current")) return;

    document.querySelectorAll(".toc .active").forEach((element) => {
      element.classList.remove("active", "active-current");
    });
    target.classList.add("active", "active-current");
    let parent = target.parentNode;
    while (!parent.matches(".toc")) {
      if (parent.matches("li")) parent.classList.add("active");
      parent = parent.parentNode;
    }
  }

  function findIndex(entries) {
    let index = 0;
    let entry = entries[index];
    if (entry.boundingClientRect.top > 0) {
      index = sections.indexOf(entry.target);
      return index === 0 ? 0 : index - 1;
    }
    for (; index < entries.length; index++) {
      if (entries[index].boundingClientRect.top <= 0) {
        entry = entries[index];
      } else {
        return sections.indexOf(entry.target);
      }
    }
    return sections.indexOf(entry.target);
  }

  function createIntersectionObserver(marginTop) {
    marginTop = Math.floor(marginTop + 10000);
    let intersectionObserver = new IntersectionObserver(
      (entries, observe) => {
        let scrollHeight = document.documentElement.scrollHeight + 100;
        if (scrollHeight > marginTop) {
          observe.disconnect();
          createIntersectionObserver(scrollHeight);
          return;
        }
        let index = findIndex(entries);
        activateNavByIndex(navItems[index]);
      },
      {
        rootMargin: marginTop + "px 0px -100% 0px",
        threshold: 0,
      }
    );
    sections.forEach((element) => {
      element && intersectionObserver.observe(element);
    });
  }
  createIntersectionObserver(document.documentElement.scrollHeight);
}

document.addEventListener("DOMContentLoaded", listennSidebarTOC);
document.addEventListener("pjax:success", listennSidebarTOC);
</script>

<!-- more -->

 
	   
	    


<script src="https://cdn.jsdelivr.net/npm/pjax@0.2.8/pjax.min.js"></script>


<script>
    var pjax;
    document.addEventListener('DOMContentLoaded', function () {
      pjax = new Pjax({
        elements: 'a[href]:not([href^="#"]):not([href="javascript:void(0)"]):not([pjax-fancybox])',
        selectors: [
          "title",
          
          "#pjax-container",
          "#pjax-header-nav-list"
        ],
        cacheBust: false,   // url 地址追加时间戳，用以避免浏览器缓存
        timeout: 5000
      });
    });

    document.addEventListener('pjax:send', function (e) {
      //window.stop(); // 相当于点击了浏览器的停止按钮

      try {
        var currentUrl = window.location.pathname;
        var targetUrl = e.triggerElement.href;
        var banUrl = [""];
        if (banUrl[0] != "") {
          banUrl.forEach(item => {
            if(currentUrl.indexOf(item) != -1 || targetUrl.indexOf(item) != -1) {
              window.location.href = targetUrl;
            }
          });
        }
      } catch (error) {}

      window.subData = null; // 移除标题（用于一二级导航栏切换处）

      volantis.$switcher.removeClass('active'); // 关闭移动端激活的搜索框
      volantis.$header.removeClass('z_search-open'); // 关闭移动端激活的搜索框
      volantis.$wrapper.removeClass('sub'); // 跳转页面时关闭二级导航

      // 解绑事件 避免重复监听
      volantis.$topBtn.unbind('click');
      $('.menu a').unbind('click');
      $(window).unbind('resize');
      $(window).unbind('scroll');
      $(document).unbind('scroll');
      $(document).unbind('click');
      $('body').unbind('click');
	  // 使用 volantis.pjax.send 方法传入pjax:send回调函数 参见layout/_partial/scripts/global.ejs
	  volantis.pjax.method.send.start();
    });

    document.addEventListener('pjax:complete', function () {
      $('.nav-main').find('.list-v').not('.menu-phone').removeAttr("style",""); // 移除小尾巴的移除
      $('.menu-phone.list-v').removeAttr("style",""); // 移除小尾巴的移除
      $('script[data-pjax], .pjax-reload script').each(function () {
        $(this).parent().append($(this).remove());
      });
      try{
		// 使用 volantis.pjax.push 方法传入重载函数 参见layout/_partial/scripts/global.ejs
		volantis.pjax.method.complete.start();
      } catch (e) {
        console.log(e);
      }
    });

    document.addEventListener('pjax:error', function (e) {
	  // 使用 volantis.pjax.error 方法传入pjax:error回调函数 参见layout/_partial/scripts/global.ejs
	  volantis.pjax.method.error.start();
      window.location.href = e.triggerElement.href;
    });
</script>
 
	  
    </div>
  </body>
</html>
